Middleware Pattern
Middleware Pattern in JS
The Middleware Pattern (also called Chain of Responsibility) passes a request through a pipeline of handlers — each handler can process, transform, or short-circuit the request.
Think of it like this:
"Every request passes through a chain of checkpoints. Each checkpoint can do its job, pass it along, or stop it."
What is Middleware Pattern?
It is a behavioral design pattern where:
- a request travels through a sequence of handlers
- each handler can act on the request, modify it, or stop the chain
- handlers are independent and can be added/removed/reordered without affecting others
You use this pattern every day — it's the core of Express, Koa, Fastify, and most HTTP frameworks.
Real-world analogy
Think of airport security:
- Ticket check — are you on the flight?
- ID check — is your identity valid?
- Baggage scan — anything suspicious?
- Body scan — final check
Each step is independent. If any step fails, you don't continue. If all pass, you board.
How Express Middleware Works
app.use((req, res, next) => {
// do something
next(); // pass to next middleware
});
req— the incoming requestres— the response objectnext— call to pass control to the next handler
This is the Middleware Pattern.
Building Your Own Middleware Pipeline
Understanding the internals helps you build custom pipelines beyond HTTP.
The pipeline
function createPipeline(middlewares) {
return async function (context) {
let index = 0;
async function next() {
if (index >= middlewares.length) return;
const middleware = middlewares[index++];
await middleware(context, next);
}
await next();
return context;
};
}
Middlewares
async function logger(ctx, next) {
console.log(`[${new Date().toISOString()}] Processing: ${ctx.action}`);
await next();
console.log(`[done] ${ctx.action}`);
}
async function authenticate(ctx, next) {
if (!ctx.user) throw new Error('Unauthenticated');
await next();
}
async function authorize(requiredRole) {
return async (ctx, next) => {
if (ctx.user.role !== requiredRole) throw new Error('Forbidden');
await next();
};
}
async function validate(schema) {
return async (ctx, next) => {
const { error } = schema.validate(ctx.body);
if (error) throw new Error(`Validation: ${error.message}`);
await next();
};
}
Usage
const pipeline = createPipeline([
logger,
authenticate,
await authorize('admin'),
]);
const ctx = {
action: 'deleteUser',
user: { id: 1, role: 'admin' },
body: { userId: 42 },
};
await pipeline(ctx);
Practical Express Middleware Examples
1) Request Logger
// middleware/logger.js
export function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
JSON.stringify({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userId: req.user?.id,
})
);
});
next();
}
app.use(requestLogger);
2) Authentication Middleware
// middleware/auth.js
import jwt from 'jsonwebtoken';
export function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
// Apply to specific routes
router.get('/orders', authenticate, getOrders);
router.post('/orders', authenticate, createOrder);
3) Role-Based Authorization
// middleware/authorize.js
export function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: `Requires one of: ${roles.join(', ')}`,
});
}
next();
};
}
// Combine auth + role check
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
router.post('/reports', authenticate, authorize('admin', 'manager'), createReport);
4) Request Validation Middleware
// middleware/validate.js
import Joi from 'joi';
export function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => d.message),
});
}
req.body = value; // use validated + sanitized value
next();
};
}
// schemas/order.js
import Joi from 'joi';
export const createOrderSchema = Joi.object({
userId: Joi.string().uuid().required(),
items: Joi.array().items(
Joi.object({
productId: Joi.string().required(),
qty: Joi.number().integer().min(1).required(),
})
).min(1).required(),
});
router.post('/orders',
authenticate,
validate(createOrderSchema),
createOrder
);
5) Rate Limiter Middleware
// middleware/rateLimit.js
const requestCounts = new Map(); // userId → { count, resetAt }
export function rateLimit({ maxRequests = 100, windowMs = 60_000 } = {}) {
return (req, res, next) => {
const key = req.user?.id || req.ip;
const now = Date.now();
const data = requestCounts.get(key);
if (!data || now > data.resetAt) {
requestCounts.set(key, { count: 1, resetAt: now + windowMs });
return next();
}
if (data.count >= maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((data.resetAt - now) / 1000),
});
}
data.count++;
next();
};
}
// Apply globally or per-route
app.use(rateLimit({ maxRequests: 200, windowMs: 60_000 }));
// Stricter limit on sensitive routes
router.post('/login', rateLimit({ maxRequests: 5, windowMs: 60_000 }), login);
6) Error Handling Middleware
Express error middleware takes 4 arguments — Express detects this signature.
// middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
console.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
userId: req.user?.id,
});
// Known app errors
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
if (err.name === 'NotFoundError') {
return res.status(404).json({ error: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' });
}
// Unknown errors — don't leak internals
res.status(500).json({ error: 'Internal server error' });
}
// Must be registered LAST
app.use(errorHandler);
7) Async Error Wrapper
Async route handlers don't automatically forward errors to next. Wrap them:
// middleware/asyncHandler.js
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Without wrapper — uncaught async errors
router.get('/orders/:id', async (req, res) => {
const order = await orderService.getOrder(req.params.id); // throws → unhandled
res.json(order);
});
// With wrapper — errors reach errorHandler middleware
router.get('/orders/:id', asyncHandler(async (req, res) => {
const order = await orderService.getOrder(req.params.id);
res.json(order);
}));
Composing Middleware in a Consistent Pattern
// controllers/orderController.js
import { authenticate } from '../middleware/auth.js';
import { authorize } from '../middleware/authorize.js';
import { validate } from '../middleware/validate.js';
import { asyncHandler } from '../middleware/asyncHandler.js';
import { createOrderSchema } from '../schemas/order.js';
const orderMiddleware = [
authenticate,
authorize('user', 'admin'),
validate(createOrderSchema),
];
router.post('/orders',
...orderMiddleware,
asyncHandler(async (req, res) => {
const order = await orderService.placeOrder(req.user.id, req.body.items);
res.status(201).json(order);
})
);
Every concern is its own middleware. The route handler contains only business logic.
Non-HTTP Use Cases
Middleware pattern isn't just for HTTP. Use it for:
Job Queue Pipeline
async function processJob(job) {
const pipeline = createPipeline([
logJob,
validateJobPayload,
deduplicateJob,
executeJob,
markJobComplete,
]);
await pipeline({ job });
}
Event Processing Pipeline
const eventPipeline = createPipeline([
enrichEventWithUserData,
filterSpamEvents,
transformEventSchema,
publishToKafka,
]);
await eventPipeline({ event: incomingEvent });
Data Import Pipeline
const importPipeline = createPipeline([
parseCsv,
validateRows,
deduplicateRows,
enrichWithExternalData,
batchInsertToDb,
sendSummaryEmail,
]);
await importPipeline({ filePath: 'orders.csv' });
Benefits
1. Separation of concerns — each middleware does one thing.
2. Composable — mix and match middlewares across routes.
3. Reusable — authenticate and rateLimit work on any route.
4. Testable — test each middleware in isolation.
test('authenticate rejects missing token', () => {
const req = { headers: {} };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
const next = jest.fn();
authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
5. Short-circuit on failure — any handler can stop the chain (return without calling next()).
When to use Middleware Pattern
- HTTP request lifecycle (auth, validation, logging, rate limiting)
- Processing pipelines (ETL, event processing, job queues)
- Plugin systems where external code hooks into your flow
- Anywhere you have a sequence of independent processing steps
When NOT to use it
- when steps have complex dependencies and must share state in non-obvious ways
- when the sequence is fixed and simple — just call functions sequentially
- when the indirection makes it hard to trace the execution flow
Interview definition (short answer)
"Middleware Pattern (Chain of Responsibility) passes a request through a pipeline of handlers. Each handler can process the request, modify it, or stop the chain. It decouples the sender from the handlers and makes behavior composable."
Formula:
Request → [Middleware1 → Middleware2 → Middleware3 → ...] → Response
↑ any can short-circuit